// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/cast/receiver/video_decoder.h"

#include <stdint.h>
#include <utility>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/values.h"
#include "media/base/video_frame_pool.h"
#include "media/base/video_util.h"
#include "media/cast/cast_environment.h"
// VPX_CODEC_DISABLE_COMPAT excludes parts of the libvpx API that provide
// backwards compatibility for legacy applications using the library.
#define VPX_CODEC_DISABLE_COMPAT 1
#include "third_party/libvpx/source/libvpx/vpx/vp8dx.h"
#include "third_party/libvpx/source/libvpx/vpx/vpx_decoder.h"
#include "third_party/libyuv/include/libyuv/convert.h"
#include "ui/gfx/geometry/size.h"

namespace media {
namespace cast {

    // Base class that handles the common problem of detecting dropped frames, and
    // then invoking the Decode() method implemented by the subclasses to convert
    // the encoded payload data into a usable video frame.
    class VideoDecoder::ImplBase
        : public base::RefCountedThreadSafe<VideoDecoder::ImplBase> {
    public:
        ImplBase(const scoped_refptr<CastEnvironment>& cast_environment, Codec codec)
            : cast_environment_(cast_environment)
            , codec_(codec)
            , operational_status_(STATUS_UNINITIALIZED)
        {
        }

        OperationalStatus InitializationResult() const
        {
            return operational_status_;
        }

        void DecodeFrame(std::unique_ptr<EncodedFrame> encoded_frame,
            const DecodeFrameCallback& callback)
        {
            DCHECK_EQ(operational_status_, STATUS_INITIALIZED);

            bool is_continuous = true;
            DCHECK(!encoded_frame->frame_id.is_null());
            if (!last_frame_id_.is_null()) {
                if (encoded_frame->frame_id > (last_frame_id_ + 1)) {
                    RecoverBecauseFramesWereDropped();
                    is_continuous = false;
                }
            }
            last_frame_id_ = encoded_frame->frame_id;

            const scoped_refptr<VideoFrame> decoded_frame = Decode(
                encoded_frame->mutable_bytes(),
                static_cast<int>(encoded_frame->data.size()));
            decoded_frame->set_timestamp(
                encoded_frame->rtp_timestamp.ToTimeDelta(kVideoFrequency));

            std::unique_ptr<FrameEvent> decode_event(new FrameEvent());
            decode_event->timestamp = cast_environment_->Clock()->NowTicks();
            decode_event->type = FRAME_DECODED;
            decode_event->media_type = VIDEO_EVENT;
            decode_event->rtp_timestamp = encoded_frame->rtp_timestamp;
            decode_event->frame_id = encoded_frame->frame_id;
            cast_environment_->logger()->DispatchFrameEvent(std::move(decode_event));

            cast_environment_->PostTask(
                CastEnvironment::MAIN,
                FROM_HERE,
                base::Bind(callback, decoded_frame, is_continuous));
        }

    protected:
        friend class base::RefCountedThreadSafe<ImplBase>;
        virtual ~ImplBase() { }

        virtual void RecoverBecauseFramesWereDropped() { }

        // Note: Implementation of Decode() is allowed to mutate |data|.
        virtual scoped_refptr<VideoFrame> Decode(uint8_t* data, int len) = 0;

        const scoped_refptr<CastEnvironment> cast_environment_;
        const Codec codec_;

        // Subclass' ctor is expected to set this to STATUS_INITIALIZED.
        OperationalStatus operational_status_;

        // Pool of VideoFrames to decode incoming frames into.
        media::VideoFramePool video_frame_pool_;

    private:
        FrameId last_frame_id_;

        DISALLOW_COPY_AND_ASSIGN(ImplBase);
    };

    class VideoDecoder::Vp8Impl : public VideoDecoder::ImplBase {
    public:
        explicit Vp8Impl(const scoped_refptr<CastEnvironment>& cast_environment)
            : ImplBase(cast_environment, CODEC_VIDEO_VP8)
        {
            if (ImplBase::operational_status_ != STATUS_UNINITIALIZED)
                return;

            vpx_codec_dec_cfg_t cfg = { 0 };
            // TODO(miu): Revisit this for typical multi-core desktop use case.  This
            // feels like it should be 4 or 8.
            cfg.threads = 1;

            DCHECK(vpx_codec_get_caps(vpx_codec_vp8_dx()) & VPX_CODEC_CAP_POSTPROC);
            if (vpx_codec_dec_init(&context_,
                    vpx_codec_vp8_dx(),
                    &cfg,
                    VPX_CODEC_USE_POSTPROC)
                != VPX_CODEC_OK) {
                ImplBase::operational_status_ = STATUS_INVALID_CONFIGURATION;
                return;
            }
            ImplBase::operational_status_ = STATUS_INITIALIZED;
        }

    private:
        ~Vp8Impl() final
        {
            if (ImplBase::operational_status_ == STATUS_INITIALIZED)
                CHECK_EQ(VPX_CODEC_OK, vpx_codec_destroy(&context_));
        }

        scoped_refptr<VideoFrame> Decode(uint8_t* data, int len) final
        {
            if (len <= 0 || vpx_codec_decode(&context_, data, static_cast<unsigned int>(len), NULL, 0) != VPX_CODEC_OK) {
                return NULL;
            }

            vpx_codec_iter_t iter = NULL;
            vpx_image_t* const image = vpx_codec_get_frame(&context_, &iter);
            if (!image)
                return NULL;
            if (image->fmt != VPX_IMG_FMT_I420) {
                NOTREACHED() << "Only pixel format supported is I420, got " << image->fmt;
                return NULL;
            }
            DCHECK(vpx_codec_get_frame(&context_, &iter) == NULL)
                << "Should have only decoded exactly one frame.";

            const gfx::Size frame_size(image->d_w, image->d_h);
            // Note: Timestamp for the VideoFrame will be set in VideoReceiver.
            // |decoded_frame| will be returned to |video_frame_pool_| on destruction to
            // be reused.
            const scoped_refptr<VideoFrame> decoded_frame = video_frame_pool_.CreateFrame(PIXEL_FORMAT_I420, frame_size,
                gfx::Rect(frame_size), frame_size,
                base::TimeDelta());
            libyuv::I420Copy(image->planes[VPX_PLANE_Y], image->stride[VPX_PLANE_Y],
                image->planes[VPX_PLANE_U], image->stride[VPX_PLANE_U],
                image->planes[VPX_PLANE_V], image->stride[VPX_PLANE_V],
                decoded_frame->visible_data(media::VideoFrame::kYPlane),
                decoded_frame->stride(media::VideoFrame::kYPlane),
                decoded_frame->visible_data(media::VideoFrame::kUPlane),
                decoded_frame->stride(media::VideoFrame::kUPlane),
                decoded_frame->visible_data(media::VideoFrame::kVPlane),
                decoded_frame->stride(media::VideoFrame::kVPlane),
                frame_size.width(), frame_size.height());
            return decoded_frame;
        }

        // VPX decoder context (i.e., an instantiation).
        vpx_codec_ctx_t context_;

        DISALLOW_COPY_AND_ASSIGN(Vp8Impl);
    };

#ifndef OFFICIAL_BUILD
    // A fake video decoder that always output 2x2 black frames.
    class VideoDecoder::FakeImpl : public VideoDecoder::ImplBase {
    public:
        explicit FakeImpl(const scoped_refptr<CastEnvironment>& cast_environment)
            : ImplBase(cast_environment, CODEC_VIDEO_FAKE)
            , last_decoded_id_(-1)
        {
            if (ImplBase::operational_status_ != STATUS_UNINITIALIZED)
                return;
            ImplBase::operational_status_ = STATUS_INITIALIZED;
        }

    private:
        ~FakeImpl() final { }

        scoped_refptr<VideoFrame> Decode(uint8_t* data, int len) final
        {
            // Make sure this is a JSON string.
            if (!len || data[0] != '{')
                return NULL;
            base::JSONReader reader;
            std::unique_ptr<base::Value> values(
                reader.Read(base::StringPiece(reinterpret_cast<char*>(data), len)));
            if (!values)
                return NULL;
            base::DictionaryValue* dict = NULL;
            values->GetAsDictionary(&dict);

            bool key = false;
            int id = 0;
            int ref = 0;
            dict->GetBoolean("key", &key);
            dict->GetInteger("id", &id);
            dict->GetInteger("ref", &ref);
            DCHECK(id == last_decoded_id_ + 1);
            last_decoded_id_ = id;
            return media::VideoFrame::CreateBlackFrame(gfx::Size(2, 2));
        }

        int last_decoded_id_;

        DISALLOW_COPY_AND_ASSIGN(FakeImpl);
    };
#endif

    VideoDecoder::VideoDecoder(
        const scoped_refptr<CastEnvironment>& cast_environment,
        Codec codec)
        : cast_environment_(cast_environment)
    {
        switch (codec) {
#ifndef OFFICIAL_BUILD
        case CODEC_VIDEO_FAKE:
            impl_ = new FakeImpl(cast_environment);
            break;
#endif
        case CODEC_VIDEO_VP8:
            impl_ = new Vp8Impl(cast_environment);
            break;
        case CODEC_VIDEO_H264:
            // TODO(miu): Need implementation.
            NOTIMPLEMENTED();
            break;
        default:
            NOTREACHED() << "Unknown or unspecified codec.";
            break;
        }
    }

    VideoDecoder::~VideoDecoder() { }

    OperationalStatus VideoDecoder::InitializationResult() const
    {
        if (impl_.get())
            return impl_->InitializationResult();
        return STATUS_UNSUPPORTED_CODEC;
    }

    void VideoDecoder::DecodeFrame(std::unique_ptr<EncodedFrame> encoded_frame,
        const DecodeFrameCallback& callback)
    {
        DCHECK(encoded_frame.get());
        DCHECK(!callback.is_null());
        if (!impl_.get() || impl_->InitializationResult() != STATUS_INITIALIZED) {
            callback.Run(make_scoped_refptr<VideoFrame>(NULL), false);
            return;
        }
        cast_environment_->PostTask(CastEnvironment::VIDEO,
            FROM_HERE,
            base::Bind(&VideoDecoder::ImplBase::DecodeFrame,
                impl_,
                base::Passed(&encoded_frame),
                callback));
    }

} // namespace cast
} // namespace media
